Objektorientiertes Programmieren
Teil 1

Vererbung, Polymorphie, Gruppenprojekt mit Gosu

Dr.-Ing. Jörg Matthes
Dipl.-Inf. Oliver Scherer

5 Objektorientierung

5.1 Private Felder, Methoden und Konstruktoren

Private Felder, Methoden und Konstruktoren

struct Foo {
    int32_t normales_feld;
private:
    int32_t privates_feld;
    // hier auch Methoden oder Konstruktoren
};

Foo foo;
cout << foo.normales_feld; // OK
cout << foo.privates_feld; // Fehler!

5.2 Klassen

Klassen

Klassen sind structs, bei denen alle Felder standardmäßig privat sind. Es muss das public keyword verwendet werden, um Konstruktoren und Methoden öffentlich zu machen.

class Foo {
    int32_t privates_feld;
public:
    Foo(int32_t wert);
};

Foo::Foo(int32_t wert) {
    this -> privates_feld = wert;
}

5.3 Vererbung

Grundlagen Vererbung


Vererbung dargestellt im Klassendiagramm


Vererbung von Attributen

Kindklasse Mitarbeiter ist eine spezielle Form von Elternklasse Person und erbt alle Attribute.

Kindklasse Mitarbeiter kann durch neue Attribute erweitert werden.

class Person {
public:
    string name;
};

class Mitarbeiter : public Person {  //Vererbung
public:
    // Attribut ´name´ wurde von Person geerbt
    int32_t personalnummer;
};


int main(){
  Person p1;
  p1.name = "Müller";

  Mitarbeiter m1;
  m1.name = "Mayer";
  m1.personalnummer = 12345;
}

Vererbung von Attributen - Achtung!

Zuweisung ist erfolgreich, "vergisst" aber alle Felder die Mitarbeiter-spezifisch sind.

Person p2 = m1;

Vererbung ist transitiv

Kindklasse Leitender_Mitarbeiter erbt alle Attribute von Person und Mitarbeiter und kann durch neue Attribute erweitert werden.

class Leitender_Mitarbeiter : public Mitarbeiter {
public:
    // Attribut name wurde über Mitarbeiter von Person geerbt
    // Attribut personalnummer wurde von Mitarbeiter geerbt
    int32_t anzahl_untergebene;
};


int main() {
  Leitender_Mitarbeiter lm1;
  lm1.name = "Schulze";
  lm1.personalnummer = 98765;
  lm1.anzahl_untergebene = 17;
}

Vererbung von Methoden

Kindklasse Mitarbeiter ist eine spezielle Form von Elternklasse Person und erbt alle Methoden.

Kindklasse Mitarbeiter kann durch neue Methoden erweitert werden.

class Person {
    string name; // private
public:
    void set_name(const string& n) {
        this->name = n;
    }
    string get_name() {
        return this->name;
    }
};

class Mitarbeiter : public Person {  // Vererbung
    int32_t personalnummer; // private
public:
    void set_personalnummer(const int32_t& pn) {
        this->personalnummer = pn;
    }
    int32_t get_personalnummer()
        return this->personalnummer;
    }
};

Vererbung von Methoden

int main() {
  Mitarbeiter m1;
  m1.set_name("Mayer");
  m1.set_personalnummer(12345);
  cout << m1.get_name() << ", " << m1.get_personalnummer() << endl;
}

Die Vererbung von Methoden ist ebenfalls transitiv!

Vererbung und Konstruktoren

Jedes Objekt der Kindklasse Mitarbeiter besitzt ein anonymes Objekt der Elternklasse Person.

Beim Erzeugen eines Objekts der Kindklasse wird also immer ein Konstruktor der Elternklasse aufgerufen

Durch

Mitarbeiter m1;

wird der vom System generierte Standardkonstruktor für Mitarbeiter aufgerufen.

Dieser ruft automatisch den vom System generierten Standardkonstruktor von Person auf.

Vererbung und Konstruktoren

Besitzen die Klassen einen allgemeinen Konstruktor (der vom System generierte Standardkonstruktor existiert dann nicht mehr), dann muss auch für das Erzeugen des anonymen Person-Objekts dessen allgemeiner Konstruktor aufgerufen werden.

class Person {
    string name; //private
public:
    Person(string n);
};

class Mitarbeiter : public Person {  //Vererbung
int32_t personalnummer; //private
public:
    Mitarbeiter(string n, int32_t pn);
};

Person::Person(string n)
  : name(n) { }

Mitarbeiter::Mitarbeiter(string n, int32_t pn)
  : Person(n), personalnummer(pn) {}

int main() {
    Mitarbeiter m1("Meyer", 12345);
}

Vererbung und Zeiger/Referenzen

Referenzen und Zeiger der Elternklasse können auch für Objekte der Kindklasse genutzt werden.

Mitarbeiter m1("Meyer", 12345);
cout << m1.get_name(); // ruft Person::get_name auf

// Referenz vom Typ Person auf Objekt vom Typ Mitarbeiter
Person& p1 = m1;
cout << p1.get_name();

// Zeiger vom Typ Zeiger-auf-Person auf Objekt vom Typ Mitarbeiter
Person* p2_ptr = &m1;
cout << p2_ptr->get_name();

Vererbung und Zugriffsschutz

5.4 Polymorphismus

Überschreiben geerbter Methoden

In einer Kindklasse können geerbte Methoden überschrieben werden:

Überschreiben geerbter Methoden

Die von Geom_Figur geerbte Methode get_flaeche wird in den Kindklassen Quadrat und Kreis überschrieben:

class Geom_Figur {
  public:
    float get_flaeche() {return 0;}
}

class Quadrat : public Geom_Figur {
  float breite;
  public:
    void set_breite(float b) {this->breite = b;}
    float get_flaeche() {return breite*breite;}
}

class Kreis : public Geom_Figur {
  float radius;
  public:
    void set_radius(float r) {this->radius = b;}
    float get_flaeche() {return 3.1415 * radius * radius;}
}

Überschreiben geerbter Methoden

Die von Geom_Figur geerbte Methode get_flaeche wird in den Kindklassen Quadrat und Kreis überschrieben.

Vorteil: Der Aufruf zur Flächenberechnung ist immer gleich, egal, um welche geometrische Figur es sich handelt.

Quadrat q1;
q1.set_breite(4);
cout << "Fläche:" << q1.get_flaeche() << endl; //16

Kreis k1;
q1.set_radius(5);
cout << "Fläche:" << k1.get_flaeche() << endl; //78.54

Virtuelle Methoden

Wenn wir nun die Flächeninhalte einer ganzen Liste von geometrischen Figuren ausgeben lassen wollen, dann müsste das doch eigentlich so funktionieren:

// Liste von Zeigern auf geometrische Objekte
vector<Geom_Figur*> Liste = {&q1, &k1};

for (size_t i = 0; i < Liste.size(); i++)
{
  cout << "Fläche:" << Liste.at(i)->get_flaeche() << endl;
}

Liefert 0 0 anstelle 16 und 78.54

Problem: Zeiger in der Liste ist vom Typ Geom_Figur*. Deshalb wird get_flaeche von Geom_Figur aufgerufen und nicht von Quadrat bzw. Kreis.

Virtuelle Methoden

Damit nicht der Zeigertyp, sondern der Objekttyp entscheidet, welche Variante der polymorphen Methode get_flaeche aufgerufen wird, muss die Methode als virtuelle Methode deklariert werden:

class Geom_Figur {
  public:
    virtual float get_flaeche() {return 0;}
}

class Quadrat : public Geom_Figur {
  float breite;
  public:
    void set_breite(float b) {this->breite = b;}
    virtual float get_flaeche() override {return breite*breite;}
}

class Kreis : public Geom_Figur {
  float radius;
  public:
    void set_radius(float r) {this->radius = b;}
    virtual float get_flaeche() override {return 3.1415 * radius * radius;}
}

Virtuelle Methoden

Virtueller Destruktor

class Geom_Figur {
public:
  Geom_Figur()  { cout << "Konstruktor Geom_Figur" << endl; }
  ~Geom_Figur() { cout << "Destruktor Geom_Figur" << endl; }
};

class Kreis : public Geom_Figur {
  float radius;
public:
  Kreis() { cout << "Konstruktor Kreis" << endl; }
  ~Kreis() { cout << "Destruktor Kreis" << endl; }
};

int main() {
  {
    // Aufruf des Konstruktors für Kreis und damit auch für Geom_Figur
    unique_ptr <Geom_Figur>  gf_ptr = make_unique <Kreis> ();
  }
  // Nur Aufruf des Destruktors für Geom_Figur,
  // da Zeiger vom Typ unique_ptr< Geom_Figur >
}

Bei Aufruf malloc im Konstruktor von Kreis und passendem free im Destruktor würde ein Memory-Leak entstehen, da free nicht aufgerufen wird.

Virtueller Destruktor

Deshalb: Destruktoren immer als virtuelle Methoden deklarieren!

class Geom_Figur {
public:
  Geom_Figur()  { cout << "Konstruktor Geom_Figur" << endl; }
  virtual ~Geom_Figur() { cout << "Destruktor Geom_Figur" << endl; }
};

class Kreis : public Geom_Figur {
  float radius;
public:
  Kreis() { cout << "Konstruktor Kreis" << endl; }
  virtual ~Kreis() override { cout << "Destruktor Kreis" << endl; }
};

Damit wird auch der Destruktor von Kreis korrekt aufgerufen.

5.5 Abstrakte Klassen

Abstrakte Klassen

In C++ sind Klassen abstrakt, wenn sie mindestens eine rein virtuelle Methode besitzen:

class Geom_Figur {
public:
  virtual float get_flaeche() = 0;
}

Abstrakte Klassen

int main() {
  Kreis k;
  Geom_Figur gf; // Compiler-Fehler, da Geom_Figur abstrakt

  Geom_Figur* gf_ptr = &k; // funktioniert weiterhin


  unique_ptr<Kreis> k_uptr = make_unique<Kreis>();

  unique_ptr<Geom_Figur> gf_uptr = move(k_uptr);

  cout << gf_uptr -> get_flaeche();
}

5.6 Assoziation zwischen Klassen

Reine Assoziation

Neben der Vererbung können Klassen auch durch Assoziationsbeziehungen miteinander verknüpt sein.

Die reine Assoziation (Benutzt-/Kennt-Beziehung, Lose Kopplung) zwischen zwei Klassen bedeutet, dass ein Objekt der assoziierenden Klasse (Heizregler) ein Objekt der assoziierten Klasse (Temperaturfühler) benutzt/kennt.

In der UML wird dies durch einen einfachen Pfeil mit Angabe der Multiplizität (z.B. 1,1) angegeben. Der Pfeil kennzeichnet dabei eine gerichtete Beziehung (Heizregler kennt Temperaturfühler, aber nicht umgekehrt.)


Reine Assoziation

In der Umsetzung bedeutet das, dass das assoziierende Objekt einen shared_ptr für das assoziierte Objekt besitzt:


class Temperaturfühler {
...
}

class Heizregler {
  shared_ptr<Temperaturfühler> Sensor_1;
}

Reine Assoziation

Bei einer Multiplizität 1,n benutzt/kennt 1 Objekt der assoziierenden Klasse n Objekte der assoziierten Klasse:


Umsetzung:


class Heizregler {
  vector<shared_ptr<Temperaturfühler>> Sensorliste;
}

Aggregation

Die Aggregation ist eine spezielle Form der Assoziation. Durch sie soll eine engere Verbindung zwischen den beteiligten Objekten ausgedrückt werden:


In der Umsetzung unterscheidet sie sich jedoch nicht von der Assoziation.

class Heizregler {
  vector<shared_ptr<Mitarbeiter>> Belegschaft;
}

Komposition

Die Komposition ist eine spezielle Form der Aggregation und damit auch der Assoziation. Durch sie soll eine sehr enge Verbindung mit Existenzabhängigkeit zwischen den beteiligten Objekten ausgedrückt werden:


Bei der Komposition existieren die kompostitionierten Objekte (Kapitel) nur solange, solange auch das kompositionierende Objekt (Buch) existiert.

(Wenn das Buch zerstört wird, existieren auch die Kapitel nicht mehr.)

Komposition

Bei der Umsetzung besitzt das kompositionierende Objekt (Buch) nun nicht mehr nur Zeiger auf, sondern die kompositionierten Objekte (Kapitel) selbst:


class Kapitel {
...
}

class Buch {
  vector<Kapitel> Inhalt;
}

Wenn der Typ des kompositionierten Objektes eine Basisklasse ist, so kann mit einem unique_ptr der Besitz umgesetzt werden.

Ungerichtete Assoziation

Bei einer ungerichteten Assoziation kennt nicht nur ein Objekt das andere, sondern die Objekte kennen sich gegenseitig (keine Pfeile im Klassendiagramm).

(Eine gegenseitige Aggregation also eine engere Bindung ist in UML nicht möglich.)


Umsetzung:


class Frau; // nur Deklaration wegen gegenseitiger Sichtbarkeit

class Mann {
  shared_ptr<Frau> Ehefrau;
}

class Frau {
  shared_ptr<Mann> Ehemann;
}

6 Grafikengine

Git installieren

(auf DHBW PCs normalerweise nicht nötig)

Neues Projekt

  1. Neuen Ordner erstellen
  2. Rechtsklick -> Git clone
  3. "https://github.com/oli-obk/dhbw-objektorientierung.git" in Url Feld eigeben
  4. Ok klicken
  5. Warten
  6. Beispielprojekt.sln (Microsoft Visual Studio Solution) öffnen

Neues Projekt

Notwendige Includes:

#include <Gosu/Gosu.hpp>
#include <Gosu/AutoLink.hpp>

Neues Projekt

class GameWindow : public Gosu::Window {
public:
  GameWindow()
    : Window(640, 480)
  {
    set_caption("Gosu Tutorial Game");
  }

  void update() override {
    // ...
  }

  void draw() override {
    // ...
  }
};

int main() {
  GameWindow window;
  window.show(); //blockierender Aufruf bis window geschlossen wird
}

Window Konstruktor

  GameWindow()
    : Window(640, 480)
  {
    set_caption("Gosu Tutorial Game");
  }

Angabe von Höhe und Breite im Konstruktor.

Aufrufen von set_caption erlaubt ersetzen des Titeltextes des Fensters

Formen zeichnen

In der draw Methode kann mittels des Graphics-Objektes gezeichnet werden.

Zugriff auf das Graphics-Objekt erhält man über die graphics()-Methode.

graphics().draw_line(
    10, 20, Gosu::Color::RED,
    200, 100, Gosu::Color::GREEN,
    0.0
);


Dokumentation

Alle Funktionen und Typen sind als Webseite dokumentiert

https://www.libgosu.org/cpp/namespace_gosu.html

Aufgabe

ERRINNERUNG

Funktionsdeklaration in der Doku: int foo(int bar) const Funktionsaufruf in eurem Code: int x = objekt.foo(y);

Wer einen Funktionsaufruf der Form int foo(int bar = y) const; abliefert, bringt nächstes Vorlesung einen Kuchen mit.

Animieren

Die draw Methode kann auf Variablen des eigenen GameWindow Objektes zugreifen. Mittels der update Methode können diese regelmäßig verändert werden.

  int x = 0;

  void update() override {
    x = (x + 1) % 300
  }

  void draw() override {
    Gosu::Graphics::draw_line(
        x, 20, Gosu::Color::RED,
        200, 100, Gosu::Color::GREEN,
        0.0
    );
  }


Benutzereingaben

Benutzereingaben können via input() abgefragt werden.

Aufgabe

Dreieck


Dreieck

  double x = 0;
  double y = 0;

  void update() override
  {
    x = input().mouse_x();
    y = input().mouse_y();
  }

  void draw() override
  {
    Gosu::Graphics::draw_triangle(
      x, y, Gosu::Color::RED,
      200, 100, Gosu::Color::GREEN,
      200, 400, Gosu::Color::BLUE,
      0.0
    );
  }

Bilder

Gosu macht das Bilder laden sehr einfach. Es gibt einen Image Typ, dessen Konstruktor einen Dateinamen als einziges Argument nimmt.

Eine Variable vom Typ Image kann mit den Funktionen draw und draw_rot gezeichnet werden.

Damit das Bild nicht 60 Mal pro Sekunde geladen wird, muss das Bild ein Feld der GameWindow Klasse sein.

Die zu ladenden Bilder müssen sich im Projektordner befinden (nicht im Solutionordner!)

Bilder laden

Gosu::Image bild;
GameWindow()
  : Window(640, 480)
  , bild("rakete.png")
{
  set_caption("Gosu Tutorial Game mit Git");
}

void draw() override
{
  bild.draw_rot(x, y, 0.0,
    0.0, // Rotationswinkel in Grad
    0.5, 0.5 // Position der "Mitte" relativ zu x, y
  );
}

Bilder - Aufgabe

Diverse Werte für den 5. und 6. Parameter auswählen und die Effekte beobachten.

Bilder drehen - Aufgabe

Tastatur- und Mauseingaben können mit der down funktion des input() Objektes abgefragt werden.

Als Argument wird ein Wert des Enums ButtonName erwartet. Finden Sie die Werte für die rechte und die linke Maustaste.

Drehen Sie ihr Bild nach rechts, wenn die rechte Maustaste gedrückt ist, und nach links, wenn die linke Maustaste gedrückt ist.

Es sollte möglich sein, durch gedrückt halten einer Maustaste das Bild komplett im Kreis zu drehen.

Errinnerung: Nur in der update Funktion dürfen Felder von GameWindow verändert werden.

Bilder drehen - Lösung


  void draw() override
  {
    bild.draw_rot(x, y, 0.0,
      rot, // Rotationswinkel in Grad
      0.5, 0.5 // Position der "Mitte"
    );
  }

  double rot = 0.0;
  double x = 0;
  double y = 0;

  void update() override
  {
    x = input().mouse_x();
    y = input().mouse_y();
    if (input().down(Gosu::MS_LEFT)) {
      rot += 10;
    }
    else if (input().down(Gosu::MS_RIGHT)) {
      rot -= 10;
    }
  }

6.1 Versionsverwaltung

Was ist Versionsverwaltung?

Austausch und Backup Server

Auf https://github.com registrieren und einloggen


Neues Git Repository anlegen

Geht auf

https://github.com/oli-obk/dhbw-objektorientierung

Und klickt auf Fork rechts oben in der Ecke

Nach erfolgreichem klonen, rechts auf "Clone or download" klicken und http Addresse kopieren.

Projekt zu git hinzufügen

Im Visual Studio kann nun das eigene Projekt mit Github verbunden werden.

Lokalen Zustand auf Server laden

Zustand auf Server


Änderungen

Zum Test eine Änderung am Programm durchführen. Zum Beispiel den Titel (set_caption) des Fensters ändern.

Im Team-Explorer kann nun unter "Änderungen" eingesehen werden, was genau sich verändert hat.

Mit Doppelklick auf eine Datei im Team-Explorer erschein ein Vergleichsfenster.

Vergleichen


Änderungen einchecken


Kurze Beschreibung der Änderungen eingeben, und "Alles einchecken" klicken

Änderungen hochladen

Visual Studio bietet direkt an, mit dem Server zu synchronisieren.


Auf "Sync" klicken

Im neuen Fenster auf "Push" klicken.

Änderungen auf Server einsehen

Auf der github Webseite des Repositorys ist nun unter "Commits" der neue Commit zu sehen.

Allen Gruppenmitgliedern Schreibzugriff geben


Einrichten

Ein Gruppenmitglied lädt sein Projekt in das neue Repository wie bereits vorgestellt.

Alle anderen führen die nun folgenden Anweisungen durch.